Qemu-kvm CPU虚拟化

Qemu-kvm cpu虚拟化简要分析

CPU 虚拟化综述

概要

本文讲述的cpu虚拟化流程基于硬件辅助的虚拟化技术,硬件辅助的虚拟化技术包括Intel virtualization Technology(Intel VT) 和 AMD的 AMD virtualization(AMD V),本文以Intel VT技术为主。关于CPU虚拟化,Intel在CPU硬件层面提供了虚拟化支持 VT-x。之所以使用硬件辅助的主要原因在于纯软件实现全虚拟化的低效性和虚拟化漏洞。

VT-x技术简介

通常,CPU支持ring0~ring3 4个等级,但是Linux只使用了其中的两个ring0,ring3。当CPU寄存器标示了当前CPU处于ring0级别的时候,表示此时CPU正在运行的是内核的代码。而当CPU处于ring3级别的时候,表示此时CPU正在运行的是用户级别的代码。当发生系统调用或者进程切换的时候,CPU会从ring3级别转到ring0级别。ring3级别是不允许执行硬件操作的,所有硬件操作都需要内核提供的系统调用来完成.
为了从CPU层面支持VT技术,Intel在 ring0~ring3 的基础上, 扩展了传统的x86处理器架构,引入了VMX模式,VMX分为root和non-root。VMM运行在VMX root模式;Guest运行在VMX non-root模式。下图给出了Intel VT-x技术的概要。
Intel VT-x
在VT-x技术中引入VMCS (Virtual-Machine Control Structure) 结构,在这个结构中分别保存了客户机的执行环境和宿主机的执行环境,在发生VM-Exit时,硬件自动保存当前的上下文环境到VMCS的客户机状态域中,同时从VMCS的宿主机状态域中加载信息到CPU中;在发生VM-Entry时,CPU自动从VMCS的客户机状态域中加载信息CPU中;这样就实现了由硬件完成上下文的切换。

VMCS

Vmcs是vmx操作模式下的一个重要结构,这里简要说一下。VMCS保存虚拟机的相关CPU状态,每个VCPU都有一个VMCS,每个物理CPU都有VMCS对应的寄存器(物理的),当CPU发生VM-Entry时,CPU则从VCPU指定的内存中读取VMCS加载到物理CPU上执行,当发生VM-Exit时,CPU则将当前的CPU状态保存到VCPU指定的内存中,即VMCS,以备下次VMRESUME。

VMLAUCH指VM的第一次VM-Entry,VMRESUME则是VMLAUCH之后后续的VM-Entry。VMCS下有一些控制域:
VMCS 控制域

vCPU的创建与初始化

在qemu中提供了一个线程供一个vcpu的创建与运行。

简述

vCPU本质是一个结构体,该结构体包括 id, 虚拟寄存器组,状态信息等等。在用户层面维护一个CPU结构体,包括vcpu fd, kvm_run等,通过ioctl前往kvm中访问。在 kvm层面,申请vCPU的结构体,保存相关的运行状态变量等等。

Qemu 层面

函数调用链:

pc_init1 -> pc_cpus_init(pcms) -> pc_new_cpu -> cpu_x86_create -> X86_CPU -> object_new -> ... -> x86_cpu_realizefn

cpu_x86_creat: 生成一个x86结构的CPU结构体,存在qemu层面,记录vcpu的fd, kvm_run状态等。

x86_cpu_realizefn -> qemu_init_vcpu(cs) -> qemu_kvm_start_vcpu

qemu_init_vcpu:初始化cpu相关属性

cpu->nr_cores = smp_cores;
cpu->nr_threads = smp_threads;
cpu->stopped = true;
...
if (kvm_enabled()) {
    qemu_kvm_start_vcpu(cpu);

qemu_kvm_start_vcpu:为cpu生成一个线程,利用该线程创建vcpu,确保vcpu创建成功

cpu->thread = g_malloc0(sizeof(QemuThread));
qemu_thread_create(cpu->thread,thread_name,qemu_kvm_cpu_thread_fn,cpu, QEMU_THREAD_JOINABLE);
while (!cpu->created) {
    qemu_cond_wait(&qemu_cpu_cond, &qemu_global_mutex);//等待cpu创建成功,关于锁还需要了解
}

在vcpu线程中调用KVM提供的 KVM_CREATE_VCPU API到KVM中申请vcpu的创建。创建成功后,建立kvm_run的映射,这样在qemu层中也可以读出kvm中vcpu的运行状态。当然,也会调用其他API从qemu中设置vcpu,例如 cpu id, TSC等等。

qemu_kvm_cpu_thread_fn -> kvm_init_vcpu -> kvm_ioctl
                                        -> kvm_arch_init_vcpu

qemu_kvm_cpu_thread_fn:

cpu->can_do_io = 1;
current_cpu = cpu;

r = kvm_init_vcpu(cpu);

cpu->kvm_run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED,
            cpu->kvm_fd, 0);//qemu和kvm建立映射,便于信息同步

/* signal CPU creation */
cpu->created = true;//标识cpu创建成功
qemu_cond_signal(&qemu_cpu_cond);//唤醒主线程

至此CPU的初始化流程在Qemu层面基本结束.这样,在qemu层面维护了一个CPU结构体,该结构体的kvm_run成员通过内存映射和KVM中的VCPU进行信息共享;通过kvm中提供的文件描述符和相关API,在qemu层面对kvm中的vcpu进行设置,达到用户对子机CPU进行配置的目标。

Kvm 层面,创建vcpu的过程

函数调用链

kvm_vm_ioctl_create_vcpu -> kvm_arch_vcpu_create-> vmx_create_vcpu -> kmem_cache_zalloc
                                                -> vmx_vcpu_setup
                         -> kvm_arch_vcpu_setup -> vcpu_load
                         -> create_vcpu_fd 

关键函数简要分析:
VPCU创建的整体流程:

static int kvm_vm_ioctl_create_vcpu(struct kvm *kvm, u32 id)
{
    vcpu = kvm_arch_vcpu_create(kvm, id);
    r = kvm_arch_vcpu_setup(vcpu);
    kvm_get_kvm(kvm);//引用计数+1,kvm_put_kvm 引用计数减1,为0时销毁kvm
    r = create_vcpu_fd(vcpu);//生成一个fd,返回到userspace中
}

kvm_arch_vcpu_create 与架构相关的申请与初始化工作,例如创建vcpu的mmu

//x86.c
struct kvm_vcpu *kvm_arch_vcpu_create(struct kvm *kvm,
                    unsigned int id)
{
        vcpu = kvm_x86_ops->vcpu_create(kvm, id);//实际调用 vmx_create_vcpu
        ...
        vmx_vcpu_setup(vmx);// 调用vmx_vcpu_setup来设置VCPU进入非根模式下寄存器的相关信息,这个函数主要是设置VMCS数据结构中客户机状态域和宿主机状态域信息,以确保在进行VM-Entry 时,VCPU可以在非根模式下正确运行,发生VM-Exit时,也可正确切换回KVM执行环境;
        vmx_vcpu_put(&vmx->vcpu);
        put_cpu();//允许抢占->执行调度->任务切换

}

// vmx.c
static struct kvm_vcpu *vmx_create_vcpu(struct kvm *kvm, unsigned int id)
{
    vmx = kmem_cache_zalloc(kvm_vcpu_cache, GFP_KERNEL_ACCOUNT);//为kvm_vcpu申请内核内存
    vmx->vpid = allocate_vpid();//分配标识符
    vmx->guest_msrs = kmalloc(PAGE_SIZE, GFP_KERNEL_ACCOUNT);
    err = alloc_loaded_vmcs(&vmx->vmcs01);
    ...
    return &vmx->vcpu;
}//该函数定义vmx,为vmx的相关成员申请内存空间以及初始化

kvm_arch_vcpu_setup 对kvm_vcpu中的数据结构进行初始化,将VCPU的信息加载到CPU中,执行 MMU的初始化工作和VCPU的复位操作

kvm_arch_vcpu_setup => kvm_mmu_setup => init_kvm_mmu => init_kvm_tdp_mmu//支持tdp时,初始化之,设置 vcpu->arch.mmu 中的属性和函数,

int kvm_arch_vcpu_setup(struct kvm_vcpu *vcpu)
{
    vcpu_load(vcpu);
    kvm_vcpu_reset(vcpu, false);
    kvm_init_mmu(vcpu, false);
}

总的来说:kvm_arch_vcpu_create 通过调用 vmx_create_vcpu 为 kvm_vcpu结构体分配空间,对vcpu的操作实质是对该结构体的操作;通过调用 vmx_vcpu_setup 初始化申请的 vcpu,包括对vmcs结构的初始化和其他虚拟寄存器的初始化。为生成的vcpu生成一个fd,返回qemu层面,qemu层面使用该fd进行标识与访问vcpu。

vCPU的运行

qemu层

上面主要讲述了创建与初始化vCPU的流程。当vCPU创建成功,一切工作就绪后,就会运行vcpu。
函数调用链:

qemu_kvm_cpu_thread_fn -> kvm_cpu_exec -> kvm_vcpu_ioctl(cpu, KVM_RUN, 0) -> ... -> kvm

kvm_cpu_exec: 调用kvm_run 运行子机; 分析exit的原因,进行处理

 run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0);

 attrs = kvm_arch_post_run(cpu, run);

if (run_ret < 0) {...}
switch (run->exit_reason) {//struct kvm_run *run = cpu->kvm_run; qemu和kvm中的kvm_run进行了内存映射
         case KVM_EXIT_IO:...
         case KVM_EXIT_SHUTDOWN:...
         case VM_EXIT_UNKWON:...    
     }while(ret==0)

qemu_kvm_cpu_thread_fn: 由于处于while循环中,处理完一次exit后又进行ioctl调用运行虚拟机并切换到客户模式

while (1) {
        if (cpu_can_run(cpu)) {
            r = kvm_cpu_exec(cpu);
            if (r == EXCP_DEBUG) {
                cpu_handle_guest_debug(cpu);
            }
        }
        qemu_kvm_wait_io_event(cpu);
    }

由代码分析可知,通过 kvm_cpu_exec 函数调用kvm提供的kvm_run接口以此来达到运行vcpu的目的。vcpu运行guest,guest发生vm exit时,首先会在kvm中进行处理,如果kvm中处理不了将会回到qemu中进行vm exit原因分析,继而进行处理。处理完成之后将会继续回到kvm中执行对应vcpu的kvm_run。

KVM层, KVM_RUN

函数调用链:

kvm_vcpu_ioctl(kvm_run) -> kvm_arch_vcpu_ioctl_run -> vcpu_run -> vcpu_enter_guest -> vmx_vcpu_run(vcpu)
->  __vmx_vcpu_run -> vmenter.S -> vmx_vmenter

在 vmenter.S执行相关的汇编指令, 调用 VMLAUNCH启动vm, VMRESUME再次进入vm. 当退出vm时,将会进行vm_exit处理。
vmx_vcpu_run:
在该函数中会配置好VMCS结构中客户机状态域和宿主机状态域中相关字段的信息,vmcs结构是由CPU自动加载与保存的;另外还会调用汇编函数,主要是KVM为guest加载通用寄存器和调试寄存器信息,因为这些信息CPU不会自动加载,需要手动加载。一切就绪后执行 VMLAUNCH或者VMRESUME指令进入客户机执行环境。另外,guest也可以通过VMCALL指令调用KVM中的服务。

vmenter.S: 将vcpu中的寄存器中的内容加载到cpu的寄存器上

/* Load guest registers.  Don't clobber flags. */
...
#ifdef CONFIG_X86_64
    mov VCPU_R8 (%_ASM_AX),  %r8
    mov VCPU_R9 (%_ASM_AX),  %r9
    mov VCPU_R10(%_ASM_AX), %r10
    mov VCPU_R11(%_ASM_AX), %r11
    mov VCPU_R12(%_ASM_AX), %r12
    mov VCPU_R13(%_ASM_AX), %r13
    mov VCPU_R14(%_ASM_AX), %r14
    mov VCPU_R15(%_ASM_AX), %r15
#endif

vmx_vcpu_run: 配置VMCS结构,cpu自动加载,通过vmcs的相关指令读来实现(vmcs_writel,vmcs_write32等等)。

vcpu_enter_guest:该函数返回1,继续保持vcpu运行,否则退回到userspace.

r = kvm_x86_ops->handle_exit(vcpu);

当 vm exit时,需要对exit的原因进行处理,根据exit reason来决定是否交由userspace处理。

vcpu_enter_guest -> vmx_handle_exit
vmx_handle_exit处理的时候,首先根据vcpu获取其对应的vmx,从vmx中获得 exit_reason.
vmx_henadle_exit->handle_io
                ->handle_rdmsr
                ->handle_ept_violation
                ->handle_vmx_instruction return 1;

vcpu_run中,当 vcpu_enter_guest 返回值小于等于0时,将会退出循环,否则将会在kvm中继续执行。退出循环之后将会将返回值返回给userspace进行处理,exit reason 被记录在 kvm run中。exit reason 被记录在 kvm run中,kvm_run被映射到qemu层面的CPU结构体中,因此可以在qemu层获得exit_reason

for (;;) {
    if (kvm_vcpu_running(vcpu)) {
        r = vcpu_enter_guest(vcpu);
    } else {
        r = vcpu_block(kvm, vcpu);
    }

if (r <= 0)
    break;

当发生VM_EXIT时,会在KVM中分析原因,之后在KVM中或者Qemu进行处理exit,然后再返回guest中执行。如果有针对guest的外部中断到来,它是如何被guest感知的呢?

中断注入VCPU的方式

当外部有中断来时,首先通过qemu, kvm模拟中断,之后调用 kvm_make_request 函数生成一个中断请求。
在vCPU RUN的环节中可知, vcpu_run -> vcpu_enter_guest->vmx_vcpu_run 进入guest.进入guest之前,在vcpu_enter_guest函数中会调用 kvm_check_reqest 检查是否有中断请求需要注入。当确认有中断需要注入时,即调用函数注入。

vcpu_enter_guest ->kvm_check_request - inject_pending_event:

if (kvm_request_pending(vcpu)) {
    if (kvm_check_request(KVM_REQ_GET_VMCS12_PAGES, vcpu))
        kvm_x86_ops->get_vmcs12_pages(vcpu);
    if (kvm_check_request(KVM_REQ_MMU_RELOAD, vcpu))
        kvm_mmu_unload(vcpu);
    ...
    if (kvm_check_request(KVM_REQ_EVENT, vcpu) || req_int_win) {
        ++vcpu->stat.req_event;
        kvm_apic_accept_events(vcpu);
        if (vcpu->arch.mp_state == KVM_MP_STATE_INIT_RECEIVED) {
            r = 1;
            goto out;
        }

        if (inject_pending_event(vcpu, req_int_win) != 0)
            req_immediate_exit = true;
        else {}
         */
        if (kvm_check_request(KVM_REQ_HV_STIMER, vcpu))
            kvm_hv_process_stimers(vcpu);
    }

vmx 注入中断:

inject_pending_event-> set_irq = vmx_inject_irq

inject_pending_event:该函数用来把中断请求写入到目标vcpu的中断请求寄存器。

    else if (!vcpu->arch.exception.pending) {
        else if (vcpu->arch.interrupt.injected)
            kvm_x86_ops->set_irq(vcpu);//call vmx_inject_irq
}
vmx_inject_irq: 该函数将会调用 vmcs_write32 指令向vmcs的 VM_ENTRY_INTR_INFO_FIELD 写入中断。

此时,中断请求已经被写入到了vcpu中去了,vmx_vcpu_run 在vcpu开始运行之前,会读取vmcs中的信息,cpu在运行时将会知道有中断请求到来,然后在guest中调用中断处理函数处理中断请求。
总的来说,qemu和kvm模拟中断之后,生成一个中断请求。vcpu在进入guest之前,会在kvm中检查是否有中断注入请求存在。如果存在,则会将该中断请求写入到vmcs中的相关中断字段中。进入guest时,即VM_ENTRY时,首先会检查相关VM_ENTRY的控制字段(控制中断与异常注入属于VM_ENTRY的控制字段),发现存在中断请求。VCPU在运行时将会调用guest中的中断处理函数处理该中断。

总结

Intel VT-x为CPU的虚拟化提供了硬件辅助技术。通过在CPU层面引入VMX 操作模式,加速 guest 执行效率。guest运行在vmx的非root模式下,host运行在vmx的root模式。Qemu本质为host上的一个进程,该进程包括主线程管理线程以及其他任务线程,包括vCPU、异步IO线程等等。对于CPU的虚拟化,首先qemu中会根据vcpu的数量,为每一个vcpu分配一个线程。该vcpu线程会通过ioctl调用kvm提供的 KVM_CREATE_VCPU API进行kvm中创建vcpu.在kVM中根据CPU的架构为特定架构的vCPU结构体申请存储空间,初始化vCPU的相关成员变量,包括vmcs、相关寄存器组。在kvm中创建成功vCPU之后,会将该结构对应的fd返回给Qemu层,这样Qemu层可以通过ioctl+fd来访问内核中的vCPU。
当vCPU创建完成之后,开始运行vCPU。在Qemu层,当检测到vCPU可以运行之后,就调用kvm提供的KVM_RUN API进入到内核中执行vCPU运行guest。guest在运行期间会发生 VM_EXIT,即从vmx的非根模式切换到根模式。如果EXIT需要被Qemu层处理,会将该EXIT注入到Qemu层面,Qemu层处理完中断之后再次返回到kvm中运行vCPU。在KVM中,为vCPU绑定一个物理CPU,执行VMLAUNCH初次进入guest中,或者执行 VMRESUME在发生VM_EXIT之后再次进入guest,该进入称为VM_ENTRY,发生VM_ENTRY时,CPU则将vCPU对应的VMCS中的字段加载到物理CPU上执行,当发生VM_Exit时,CPU则将当前的CPU状态保存到VCPU对应的VMCS中,以备下次VMRESUME。
guest在运行期间会发生VM_EXIT,导致VM_EXIT的原因有:执行了会导致VM_EXIT的特权指令、guest中的中断或者异常、外部中断、CPU任务调度等等。发生VM_EXIT之后,首先会在KVM中进行处理,KVM中处理不了的中断将会交给Qemu层面进行处理。一般来说,读取CPU的msr寄存器、vmx指令操作等操作会直接在kvm中处理,由IO、MMIO、内部错误等exit会视情况交由Qemu层面处理。外部中断会导致VMEXIT。关于外部中断注入vCPU的过程,首先会在Qemu和kVM中模拟中断,然后在KVM中生成一个中断请求。发生VMEXIT之后再次进入guest之前会检查是否有中断请求,如果存在中断请求,调用vmcs_write32 指令向vmcs相应字段写入中断,此时中断被注入到了vCPU。在VM_ENTRY环节会读取该中断请求到vcpu绑定的CPU上。这样CPU在运行环节就知道有中断到来,调用对应的中断处理函数处理中断。

附录

导致 vm-exits 的指令

  1. 无条件退出
    CPUID, INVD, MOV from CR3, VMCALL, VMCLEAR, VMLAUNCH, VMPTRLD, VMPTRST, VMREAD, VMRESUME, VMWRITE, VMXOFF, VMXON
  2. 有条件退出
    某条指令是否退出依赖于 VM-execution controls的设置。特权指令执行时触发异常,当有异常发生时,在其中断向量表查找Exception Bitmap对应的标志位,如果该标志位为1,该异常将会导致 vm exit。下面给出Bitmap的简介图,由32位构成,也即 VM-Excution的控制域。
     Definitions of Processor-Based VM-Execution Controls

参考资料

https://www.cnblogs.com/lsh123/p/8470914.html
https://rayanfam.com/topics/hypervisor-from-scratch-part-5/
https://blog.csdn.net/wanthelping/article/details/47068775